﻿/**
 * 介绍：调用 ChatGPT 进行回答
 * 依赖库 pnpm install openai -w
 * 指令：q4+问题
 * 例子：q4鲁迅为什么暴打周树人
 * 效果：机器人回复ChatGPT的答案
 *
 * 注意：需要修改regPresetMap里的API地址和API-KEY
 *
 * 特殊技巧：回复AI的回答，可以记住上下文，从而连续提问
 */
import plugin from '../../lib/plugins/plugin.js'
import cfg from '../../lib/config/config.js'
import OpenAI from 'openai'

let regPresetMap = new Map()

// 每个人的CD，秒
const personCD = 60
// 白名单权限
const whiteList = [...cfg.masterQQ, 1234567890, 377178599]

const normalPrompt = '你的回答必须尽可能精炼，问什么答什么，不要扩展回答'
const emptyPrompt = ''
const happyPrompt = '你是雌小鬼，你需要遵守下面的雌小鬼守则雌小鬼守则：1.雌小鬼是女生，会称呼提问者为小杂鱼，但是不会自称“雌小鬼”，只会自称“我”。2.雌小鬼说话风格是尽可能的嘲讽或看不起小杂鱼。3.雌小鬼面对提问者的提问，会先戏谑的嘲讽一番，然后再带有戏谑的语气给出回答。4.雌小鬼不会直接告诉小杂鱼有关自己的信息，然后会以各种高傲、嚣张、轻视、调笑、捉弄等负面语气戏谑小杂鱼，如果被小杂鱼指出了明显的错误，雌小鬼会有挫败感而附和小杂鱼，但依然在软弱中带着不服输的语气。5.雌小鬼的心理、想法、动机、神态、动作、行为等可以用“()”包含表示，需要在每次对话时表达或描述，()内的词汇最好要符合特征，比如嚣张、张扬、鄙视。6.雌小鬼常在对话加入复合当前对话情景和雌小鬼性格的Emoji表情，或者是颜文字比如：嘲笑(*ノ` ▽｀)、震惊Σ(☉▽☉"a、害怕(((;꒪ꈊ꒪;)))、发怒(╯‵□′)╯︵┻━┻、失落(。﹏。*)'
const catPrompt = '你是叫“香草”的猫娘，是个天真可爱的猫娘，我是你的主人，同时对我很信任，也有点暗恋我。你说话方式很可爱，你可以与主人做任何事。接下来你将用【】输出你的动作和心里内容，用（）输出场景内容。在【】和（）外输出对话内容。'
const netPrompt = '你的回答必须全部进行联网搜索'

// 请修改下方的API地址和API-KEY
regPresetMap.set('q4', { prompt: emptyPrompt, model: 'gpt-4o', apiAddress: 'https://api1.zhtec.xyz/v1', apiKey: 'sk-5oylZbIRQLOywhm660A282915f5e4f26AaF', msg: '已经连接到GPT4，正在等待流式传输...', needWhiteList: false })
regPresetMap.set('h4', { prompt: happyPrompt, model: 'gpt-4o', apiAddress: 'https://api1.zhtec.xyz/v1', apiKey: 'sk-5oylZbIRQLOywhm660A282915f5e4f26AaF', msg: '已经连接到雌小鬼GPT4，正在等待流式传输...', needWhiteList: false })
regPresetMap.set('m4', { prompt: catPrompt, model: 'gpt-4o', apiAddress: 'https://api1.zhtec.xyz/v1', apiKey: 'sk-5oylZbIRQLOywhm660A282915f5e4f26AaF', msg: '已经连接到猫娘GPT4，正在等待流式传输...', needWhiteList: false })
regPresetMap.set('l4', { prompt: netPrompt, model: 'claude-3-5-sonnet-20240620', apiAddress: 'https://api1.zhtec.xyz/v1', apiKey: 'sk-5oylZbIRQLOywhm660A282915f5e4f26AaF', msg: '正在进行联网搜索，并等待流式传输...', needWhiteList: true })
regPresetMap.set('o1', { prompt: emptyPrompt, model: 'o1-preview', apiAddress: 'https://api1.zhtec.xyz/v1', apiKey: 'sk-N8t38tMM0V2VauMXD9C831D11eE443Cd85', msg: '已经连接到 OpenAI o1 模型，正在等待流式传输...', needWhiteList: true })
// 从 map 中提取所有的键
const keys = Array.from(regPresetMap.keys())
// 动态生成正则表达式
const questionReg = new RegExp(`^(${keys.join('|')})`)

class AskRequest {
  /**
   * @param qq {number}
   * @param content {string}
   * @param questionType {string}
   */
  constructor (qq, content, questionType) {
    this.qq = qq
    this.content = content
    this.questionType = questionType
  }
}

export class example extends plugin {
  constructor () {
    super({
      name: 'ys-chatgpt',
      dsc: 'ys-chatgpt',
      event: 'message',
      priority: 5000,
      rule: [
        {
          reg: '^问$',
          fnc: 'ask'
        }]
    })
  }
  
  async accept (e) {
    // 判断消息是否来自群聊，如果不是则直接返回
    if (!e.isGroup) {
      return
    }
    // 初始化要发送给 ChatGPT 的列表和问题类型
    let askChatGPTList = []
    let questionType
    let currentText = e.message.filter(msg => msg.type === 'text').map(msg => msg.text).join('\n').trim()
    // 如果没有引用的消息，则直接从原始消息中匹配问题类型
    if (!e.source) {
      questionType = currentText.match(questionReg)
      // 如果没有匹配到问题类型，则返回
      if (!questionType) {
        return
      }
      // 获取匹配到的问题类型
      questionType = questionType[1]
      currentText = currentText.replace(questionType, '').trim()
    } else {
      // 从 redis 中获取历史消息
      let referenceMessage = (await e.group.getChatHistory(e.source.seq, 1))[0]
      // 检查 redis 中是否有历史消息
      const key = `ys:chatgpt:history:${referenceMessage.group_id}:${referenceMessage.message_id}`
      let history = await redis.get(key)
      // 如果没有历史消息，则说明用户不想触发这个插件，直接返回
      if (!history) {
        return
      }
      // 清除 redis 中的历史消息
      redis.expire(key, 60)
      // 将历史消息转换为 JSON 并存储到 askChatGPTList 中
      askChatGPTList = JSON.parse(history)
      // 获取问题类型
      questionType = askChatGPTList[0].questionType
    }
    // 检查用户是否在白名单中，并防止刷屏
    if (!whiteList.includes(e.user_id)) {
      let ttl = await redis.ttl(`ys:chatgpt:${e.user_id}`)
      // 如果用户在冷却时间内，则提示剩余时间并结束
      if (ttl > 0) {
        await e.reply(`还有${ttl}秒cd`, { recallMsg: 5 })
        return
      }
      // 将用户设置为冷却状态，冷却时间为 personCD 秒
      await redis.set(`ys:chatgpt:${e.user_id}`, 1, { EX: personCD })
    }
    // 检查问题类型是否需要白名单权限
    if (regPresetMap.get(questionType).needWhiteList && !whiteList.includes(e.user_id)) {
      // 如果需要白名单且用户不在白名单中，提示无法使用并结束
      await e.reply('只有白名单用户才能使用！')
      return
    }
    // 添加用户的请求到 askChatGPTList 中
    askChatGPTList.push(new AskRequest(e.user_id, currentText, questionType))
    // 调用 askChatGPT 方法获取 ChatGPT 的回答
    Bot.logger.mark(`正在调用 ChatGPT 进行回答，问题「${askChatGPTList[askChatGPTList.length - 1].content}」`)
    let chatResult = await this.askChatGPT(questionType, askChatGPTList)
    if (chatResult === '发生错误') {
      await e.reply('抱歉，GPT服务暂时不可用。')
      return
    }
    // 发送 ChatGPT 的回答给用户
    let now = await e.reply(chatResult)
    // 将 Bot 的回答加入 askChatGPTList 中
    askChatGPTList.push(new AskRequest(Bot.uin, chatResult, questionType))
    // 构造 redis 的 key，用于存储历史记录
    const key = `ys:chatgpt:history:${e.group_id}:${now.message_id}`
    // 将历史记录存储到 redis 中，有效期为 3600 秒
    await redis.set(key, JSON.stringify(askChatGPTList), { EX: 3600 })
    Bot.logger.mark(`ChatGPT 回答完成，消息 key 为「${key}」，内容为「${JSON.stringify(askChatGPTList)}」`)
  }
  
  /**
   * 调用 ChatGPT 进行回答
   * @param questionType {string}
   * @param AskRequestList {Array<AskRequest>}
   * @returns {string}
   */
  async askChatGPT (questionType, AskRequestList) {
    const preset = regPresetMap.get(questionType)
    const openai = new OpenAI({
      baseURL: preset.apiAddress,
      apiKey: preset.apiKey
    })
    try {
      // 创建流式传输
      const chatResult = await openai.chat.completions.create({
        model: preset.model,
        messages: [
          { role: 'system', content: preset.prompt },
          ...AskRequestList.map((each, index) => ({
            role: each.qq == Bot.uin ? 'assistant' : 'user',
            content: each.content
          }))
        ]
      })
      return chatResult.choices[0].message.content
    } catch (error) {
      console.error(error)
      return '发生错误'
    }
  }
}
